﻿using System;
using System.Reflection;
using Sencha.Direct;
using System.Web.Mvc;
using System.Collections.Generic;
using System.Web.Routing;
using Sencha.Serialization;

namespace Sencha.Direct.Api
{
	public class ControllerDescription
	{
		private ControllerDescriptor m_descriptor;
		private string m_routerUrl;
		private Dictionary<string, ActionDescription> m_actionByName;

		public ControllerDescriptor Descriptor { get { return m_descriptor; } }
		public string Name { get; set; }
		public string Namespace { get; set; }
		public CacheParameters Caching { get; private set; }
		public int TimeoutInMilliseconds { get; private set; }

		public bool IsAsync { get; private set; }
		public bool IsDirectController { get; private set; }
		public bool IsDirectRouter { get; private set; }
		public bool IsDirectOnly { get; private set; }
		public bool IsJsonP { get; private set; }
		public bool IsNonDirect { get; private set; }
		public bool IsParallel { get; private set; }
		public bool IsChildActionOnly { get; private set; }
		public bool IsInputValidated { get; private set; }
		public ICollection<string> AcceptVerbs { get; private set; }

		public ICollection<ActionDescription> Actions { get { return m_actionByName.Values; } }
		public ICollection<string> ActionsNames { get { return m_actionByName.Keys; } }

		public ControllerDescription(ControllerDescriptor controllerDescription)
		{
			if (controllerDescription == null)
				throw new ArgumentNullException("controllerDescription");
			m_descriptor = controllerDescription;

			this.Name = controllerDescription.ControllerName;
			this.Namespace = controllerDescription.ControllerType.Namespace;
			this.IsDirectController = typeof(DirectController).IsAssignableFrom(controllerDescription.ControllerType);
			this.IsAsync = this.IsDirectController || typeof(System.Web.Mvc.Async.IAsyncController).IsAssignableFrom(controllerDescription.ControllerType);
			this.IsDirectRouter = this.IsDirectController && typeof(Router).IsAssignableFrom(controllerDescription.ControllerType);
			this.IsDirectOnly = DirectOnlyAttribute.IsDefined(controllerDescription);
			this.IsJsonP = AllowJsonPAttribute.IsDefined(controllerDescription);
			this.IsParallel = AllowParallelAttribute.IsDefined(controllerDescription.ControllerType);
			this.IsChildActionOnly = controllerDescription.IsDefined<ChildActionOnlyAttribute>(true);
			this.IsInputValidated = controllerDescription.IsDefined<ValidateInputAttribute>(true);
			this.IsNonDirect = controllerDescription.IsDefined<NonDirectActionAttribute>(true);

			if (controllerDescription.IsDefined(typeof(OutputCacheAttribute), true))
				this.Caching = new CacheParameters((OutputCacheAttribute)controllerDescription.GetCustomAttributes(typeof(OutputCacheAttribute), true)[0]);

			this.TimeoutInMilliseconds = controllerDescription.GetAttributeValue<AsyncTimeoutAttribute, int?>(true, a => a.Duration) ??
				controllerDescription.GetAttributeValue<NoAsyncTimeoutAttribute, int?>(true, a => a.Duration) ?? 0;

			if (controllerDescription.IsDefined(typeof(AcceptVerbsAttribute), true))
				this.AcceptVerbs = ((AcceptVerbsAttribute)controllerDescription.GetCustomAttributes(typeof(AcceptVerbsAttribute), true)[0]).Verbs;
			else if (controllerDescription.IsDefined(typeof(HttpPostAttribute), true))
				this.AcceptVerbs = new string[] { "POST" };
			else if (controllerDescription.IsDefined(typeof(HttpGetAttribute), true))
				this.AcceptVerbs = new string[] { "GET" };
			else if (controllerDescription.IsDefined(typeof(HttpPutAttribute), true))
				this.AcceptVerbs = new string[] { "PUT" };
			else if (controllerDescription.IsDefined(typeof(HttpDeleteAttribute), true))
				this.AcceptVerbs = new string[] { "DELETE" };

			if (this.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) && this.Name.Length > 10)
				this.Name = this.Name.Substring(0, this.Name.Length - 10);

			m_actionByName = new Dictionary<string, ActionDescription>();
			foreach (var action in controllerDescription.GetCanonicalActions())
			{
				var description = new ActionDescription(this, action);
				DescriptionCache.PutActionDescription(action, description);
				m_actionByName.Add(description.Name, description);
			}

		}

		public string GetRouterUrl(RequestContext requestContext, RouteCollection routes)
		{
			if (requestContext == null)
				throw new ArgumentNullException("requestContext");
			if (routes == null)
				throw new ArgumentNullException("routes");

			if (m_routerUrl != null)
				return m_routerUrl;

			var controllerName = this.Name;
			var actionName = "Action";
			var areaName = (string)null;
			var controllerNamespace = this.Namespace;

			if (this.Actions.Count > 0)
				actionName = System.Linq.Enumerable.First(this.Actions).Name;

			var controllerRoute = ControllerDescription.GetControllerRouteData(requestContext, routes, areaName, controllerName, actionName, controllerNamespace);
			if (controllerRoute == null)
				return null;

			controllerName = "Router";
			actionName = "Route";
			areaName = GetArea(controllerRoute);
			var vPathData = ControllerDescription.GetControllerRouteData(requestContext, routes, areaName, controllerName, actionName, null);

			if (vPathData == null)
				return null;

			if (vPathData != null)
				m_routerUrl = vPathData.VirtualPath;
			else
				m_routerUrl = null;

			if (m_routerUrl != null && vPathData.VirtualPath.StartsWith("~"))
				m_routerUrl = System.Web.VirtualPathUtility.ToAbsolute(vPathData.VirtualPath);
			else if (m_routerUrl != null && !m_routerUrl.StartsWith("/"))
				m_routerUrl = "/" + m_routerUrl;

			return m_routerUrl;
		}

		private static VirtualPathData GetControllerRouteData(RequestContext requestContext, RouteCollection routes, string areaName, string controllerName, string actionName, string controllerNamespace)
		{
			if (requestContext == null)
				throw new ArgumentNullException("requestContext");
			if (routes == null)
				throw new ArgumentNullException("routes");
			if (string.IsNullOrEmpty(controllerName))
				throw new ArgumentNullException("controllerName");
			if (string.IsNullOrEmpty(actionName))
				throw new ArgumentNullException("actionName");

			var nameAlt = !controllerName.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) ? controllerName + "Controller" : controllerName;

			var foundRouteData = (VirtualPathData)null;
			var foundRouteDataPriority = int.MaxValue;

			foreach (var routeBase in routes)
			{
				if (!(routeBase is Route))
					continue;

				var route = routeBase as Route;
				if (!(route.RouteHandler is MvcRouteHandler))
					continue;

				requestContext = new RequestContext(requestContext.HttpContext, new RouteData(route, route.RouteHandler));
				requestContext.RouteData.Values["controller"] = controllerName;
				requestContext.RouteData.Values["action"] = actionName;

				var area = GetArea(route);
				var namespaces = route.DataTokens["Namespaces"] as IEnumerable<string>;
				var isAreaRoute = !string.IsNullOrEmpty(areaName);
				var isAreaNamespaceFallback = route.DataTokens["UseNamespaceFallback"] is bool ? (bool)route.DataTokens["UseNamespaceFallback"] : false;
				var isInNamespace = IsInNamespace(namespaces, controllerNamespace);
				var wasRouted = route.GetVirtualPath(requestContext, requestContext.RouteData.Values) != null;
				var isValidNamespace = isInNamespace || (isAreaRoute && isAreaNamespaceFallback) || (!isAreaRoute && namespaces == null) || controllerNamespace == null;
				var isValidArea = string.IsNullOrEmpty(areaName) || string.Equals(areaName, area, StringComparison.OrdinalIgnoreCase);
				var isStrictMatch =
					route.Defaults.ContainsKey("controller") &&  // has default controller
					string.Equals(route.Defaults["controller"] as string, controllerName, StringComparison.OrdinalIgnoreCase) && // has default controller = this controller
					route.Url.IndexOf("{controller}") == -1; // has no controller placeholder

				if (!wasRouted || !isValidNamespace || !isValidArea)
					continue;

				// priorities
				// 1. namespace match, has strict default match, in area
				// 2. namespace match, has strict default match
				// 3. namespace match, in area
				// 4. has strict default match
				// 5. namespace match
				// 6. sucessfully routed

				var priority =
					isInNamespace && isStrictMatch && isAreaRoute ? 1 :
					isInNamespace && isStrictMatch ? 2 :
					isInNamespace && isAreaRoute ? 3 :
					isStrictMatch ? 4 :
					isInNamespace ? 5 :
					6;

				if (priority >= foundRouteDataPriority)
					continue;

				foundRouteDataPriority = priority;
				foundRouteData = route.GetVirtualPath(requestContext, requestContext.RouteData.Values);
			}

			if (foundRouteData == null)
				return null;

			return foundRouteData;
		}
		private static bool IsInNamespace(IEnumerable<string> namespaces, string targetNamespace)
		{
			if (namespaces == null || targetNamespace == null)
				return false;

			var nsSet = new HashSet<string>(namespaces);

			if (nsSet.Count == 0)
				return false;

			foreach (var ns in nsSet)
			{
				if (IsNamespaceMatch(ns, targetNamespace))
					return true;
			}
			return false;
		}
		private static bool IsNamespaceMatch(string requestedNamespace, string targetNamespace)
		{
			if (requestedNamespace == null)
				return false;
			if (requestedNamespace.Length == 0)
				return true;
			if (!requestedNamespace.EndsWith(".*", StringComparison.OrdinalIgnoreCase))
				return string.Equals(requestedNamespace, targetNamespace, StringComparison.OrdinalIgnoreCase);
			requestedNamespace = requestedNamespace.Substring(0, requestedNamespace.Length - ".*".Length);
			return targetNamespace.StartsWith(requestedNamespace, StringComparison.OrdinalIgnoreCase) && (requestedNamespace.Length == targetNamespace.Length || (int)targetNamespace[requestedNamespace.Length] == 46);
		}
		private static string GetArea(Route route)
		{
			return (string)route.DataTokens["area"];
		}
		private static string GetArea(VirtualPathData routeData)
		{
			return (string)routeData.DataTokens["area"];
		}

		public void SerializeTo(IJsonWriter writer)
		{
			writer.WriteObjectBegin();
			writer.WriteMember(this.Name);
			writer.WriteArrayBegin();
			foreach (var action in this.Actions)
			{
				if (action.IsIgnored)
					continue;

				action.SerializeTo(writer);
			}
			writer.WriteArrayEnd();
			writer.WriteObjectEnd();
		}


		public override string ToString()
		{
			return this.Name;
		}
	}
}
